Skip to content

fix: render extension skills for target agent, not just active agent …#3599

Open
Nimraakram22 wants to merge 3 commits into
github:mainfrom
Nimraakram22:fix/2948-agent-aware-skill-rendering
Open

fix: render extension skills for target agent, not just active agent …#3599
Nimraakram22 wants to merge 3 commits into
github:mainfrom
Nimraakram22:fix/2948-agent-aware-skill-rendering

Conversation

@Nimraakram22

Copy link
Copy Markdown
Contributor

Fixes #2948.

Problem

Extension skill rendering was scoped to the active agent (init-options.json's ai field) even when registering extensions for a different, non-active agent — e.g. after specify integration install copilot --integration-options "--skills" while Claude remained active, or specify integration upgrade copilot. The target agent received command files instead of SKILL.md files.

Root cause

ExtensionManager._get_skills_dir() and _register_extension_skills() always resolved the skills directory/settings from the active agent in init-options.json, ignoring the agent_name argument passed to register_enabled_extensions_for_agent().

Fix

  • Added is_agent_skills_enabled() to resolve per-agent skills-mode from .specify/integration.json's integration_settings[agent].parsed_options.skills, falling back to the legacy global ai_skills flag for the active agent.
  • Threaded agent_name through _get_skills_dir() and _register_extension_skills().
  • register_enabled_extensions_for_agent() now computes skills_mode_active per the target agent, and always attempts skill registration for that agent (matching prior best-effort error handling).

Testing

  • Manually verified: specify integration upgrade copilot --force with a non-active, skills-mode Copilot integration now renders .github/skills/speckit-git-*/SKILL.md files instead of command files, with no regression to the active agent's own skills.
  • pytest tests/ -k extension: 616/616 pass.
  • Full suite: 44 pre-existing failures unrelated to this change (Windows symlink-privilege and PowerShell path-resolution issues — confirmed present on main without this change).

…ithub#2948)

Extension skill rendering was previously scoped only to the active
agent (init-options.json's 'ai' field), even when
register_enabled_extensions_for_agent was called for a different,
non-active agent (e.g. after 'integration install <agent>
--skills' or 'integration upgrade <agent>' for a secondary
integration). This meant a skills-mode agent that wasn't the
active one received command files instead of skills.

Changes:
- Add is_agent_skills_enabled() in _init_options.py: resolves
  per-agent skills-mode from .specify/integration.json's
  integration_settings[agent].parsed_options.skills, falling back
  to the legacy global init-options ai_skills flag for the active
  agent.
- ExtensionManager._get_skills_dir() now accepts an optional
  agent_name and resolves the skills directory for that specific
  agent instead of always using the active agent.
- ExtensionManager._register_extension_skills() now accepts and
  threads through agent_name.
- register_enabled_extensions_for_agent() now computes
  skills_mode_active per the target agent_name (not the active
  agent), and always attempts skill registration for that agent
  (matching the prior best-effort error handling), while still
  skipping duplicate command-file registration when the target is
  in skills mode.

Verified manually: 'specify integration upgrade copilot --force'
with a non-active, skills-mode Copilot integration now renders
.github/skills/speckit-git-*/SKILL.md files instead of command
files, with no change to the active agent's own skills.

Test suite: 616/616 extension-related tests pass. Full suite has
44 pre-existing failures unrelated to this change (Windows
symlink-privilege and PowerShell path-resolution issues,
confirmed present on main via git stash).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Makes extension skill rendering aware of the target integration rather than only the active agent.

Changes:

  • Resolves per-agent skills mode from integration state.
  • Threads target agent through skill-directory resolution and rendering.
  • Updates extension re-registration behavior and documentation.
Show a summary per file
File Description
src/specify_cli/_init_options.py Adds per-agent skills-mode resolution.
src/specify_cli/extensions/__init__.py Targets extension skill generation by agent.
src/specify_cli/integrations/_helpers.py Updates registration behavior documentation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comments suppressed due to low confidence (2)

src/specify_cli/extensions/init.py:980

  • The target-agent branch bypasses the repository's safe directory creation. resolve_agent_skills_dir only constructs a path, and _ensure_usable follows symlinked parents with mkdir; _register_extension_skills then writes SKILL.md beneath it. A project with (for example) a symlinked .github/skills can therefore make registration write outside the project. Route this path through _ensure_safe_shared_directory as resolve_active_skills_dir does (see shared_infra.py:163-196) before writing.
                skills_dir = resolve_agent_skills_dir(self.project_root, agent_name)

src/specify_cli/extensions/init.py:1856

  • registered_skills is still a single global list of names, so merging a second agent's identically named skills loses which directories own them. For example, after Claude and non-active Copilot both render speckit-git-feature, uninstalling either agent removes its copy and clears the list in unregister_agent_artifacts; the other copy remains but is no longer tracked, so later extension removal leaves it orphaned. Track generated skills per agent (with legacy-list compatibility) before enabling multi-agent skill rendering.
                        existing_skills = self._valid_name_list(
                            metadata.get("registered_skills", [])
                        )
                        merged_skills = list(
                            dict.fromkeys(existing_skills + registered_skills)
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment thread src/specify_cli/extensions/__init__.py
Comment thread src/specify_cli/_init_options.py

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback and address test & lint errors

…thub#2948)

Addresses PR review feedback: install_from_directory() (the
'extension add' / dev-install path) still called
_register_extension_skills() with no agent_name, so it remained
scoped to only the active agent, unlike
register_enabled_extensions_for_agent() which was already fixed
to target a specific agent.

Changes:
- Add ExtensionManager._register_extension_skills_for_installed_agents(),
  which renders skills for the active agent (legacy single-agent
  projects) plus every agent listed in
  .specify/integration.json's installed_integrations, for whichever
  of those are individually in skills mode. Failures for one agent
  are warned and do not block the others.
- install_from_directory() now calls this helper instead of a
  single active-agent-only call.
- _get_skills_dir()'s agent_name branch now mirrors
  resolve_active_skills_dir()'s safety checks
  (_ensure_safe_shared_directory symlink/containment/is-a-directory
  validation, plus the Kimi native-skills-dir-must-already-exist
  fallback) instead of resolving the naive per-agent path
  unchecked — fixes a regression this exposed in the Hermes
  marker-file detection test and restores exact parity with the
  active-agent path.

Tests:
- Added TestNonActiveAgentSkillRegistration with 3 new tests
  covering register_enabled_extensions_for_agent for a non-active
  skills-mode agent, isolation from the active agent's own skills,
  and extension add rendering skills for all installed
  skills-mode agents.
- pytest tests/ -k extension: 619/619 pass (616 previous + 3 new).
- Manually verified: 'specify extension add git' with Claude
  active and Copilot installed in --skills mode now renders
  SKILL.md files for both agents ('5 agent skill(s)
  auto-registered'), with no change to Claude's own skills.
- git diff --check: clean, no whitespace errors.
@Nimraakram22

Copy link
Copy Markdown
Contributor Author

@mnriem Thanks for the review! Pushed a follow-up commit addressing both points:

  • Copilot's extension add gap: install_from_directory() now uses a new _register_extension_skills_for_installed_agents() helper that renders skills for the active agent and every agent in .specify/integration.json's installed integrations that has skills mode enabled — not just the active one. Verified manually: specify extension add git with Claude active + Copilot installed in --skills mode now reports "5 agent skill(s) auto-registered" and creates SKILL.md files under both .claude/skills/ and .github/skills/.
  • Test coverage: added TestNonActiveAgentSkillRegistration (3 tests) covering the non-active-agent upgrade path, isolation from the active agent's own skills, and the extension add multi-agent path.

Also had to fix a regression this exposed: my agent-specific _get_skills_dir() branch was resolving the naive per-agent directory without the same symlink/containment/is-a-directory safety checks that resolve_active_skills_dir() applies for the active agent this broke the Hermes marker-file detection test, now fixed to mirror that logic exactly.

pytest tests/ -k extension: 619/619 pass. git diff --check: clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (1)

src/specify_cli/extensions/init.py:1569

  • Adding this second skill pass does not stop the preceding register_commands_for_all_agents pass from registering command files for the same skills-mode agent. For example, if .github/agents already exists (often because it contains user agents), a persisted Copilot --skills install receives both .agent.md files and SKILL.md, violating the acceptance criterion. Filter per-agent skills-mode integrations out of command registration rather than merely adding skills afterward.
        # Auto-register extension commands as agent skills for every
        # skills-mode agent detected (active agent plus any other
        # installed integrations in skills mode), not just the active
        # agent (#2948).
        registered_skills = self._register_extension_skills_for_installed_agents(
            manifest, dest_dir, link_outputs=link_commands
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +405 to +410
Extension *skill* rendering is scoped to ``agent_key`` itself (using
per-agent settings in ``.specify/integration.json`` when available, with
a fallback to the legacy global init-options for the active agent), so a
skills-mode agent registered while it is not the active agent (e.g.
Copilot ``--skills`` registered while non-active) still receives skill
files here instead of only command files (#2948).
Comment on lines +1273 to +1276
for skill_name in agent_skills:
if skill_name not in seen:
seen.add(skill_name)
combined.append(skill_name)
@mnriem

mnriem commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback and fix test & lint errors

Addresses Copilot review feedback: remove() called
_unregister_extension_skills() without a directory, so its fast
path removed skill files only from the resolved active-agent
directory whenever one resolved, and never scanned the
directories of other agents whose skills were rendered via the
new multi-agent registration paths added earlier in this PR —
leaving those orphaned on the filesystem after removal.

_unregister_extension_skills() now always scans every known
agent skills directory (previously this broader scan only ran
as a fallback when no active-agent directory could be resolved
at all), in addition to any explicitly-passed or resolved
directory. Each candidate directory's matching skill
subdirectories are still verified against the SKILL.md
metadata.source field before removal, so this doesn't touch
the existing safety guarantees or the registered_skills schema
(intentionally kept as a flat, deduped list rather than a
per-agent dict, to avoid a breaking registry-schema change for
this fix).

Tests:
- Added test_remove_cleans_up_skills_for_non_active_agent,
  covering removal after skills were rendered for both an
  active and a non-active agent.
- pytest tests/ -k extension: 620/620 pass (619 previous + 1 new).
- git diff --check: clean.

Note: a related Copilot suggestion to also have plain
'integration install' immediately register extension
commands/skills for the newly-installed agent was evaluated but
not applied here, since it directly conflicts with the
maintainer-requested, explicitly-tested deferred-registration
design from github#2886 (see
tests/integrations/test_integration_subcommand.py::TestIntegrationInstall::test_install_defers_extension_commands_until_use).
Flagging this for maintainer input rather than silently
reversing that prior decision.
@Nimraakram22

Nimraakram22 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@mnriem Pushed another commit addressing Copilot's second point (skill cleanup across agents):

  • Orphaned skills on remove: _unregister_extension_skills() previously only scanned every known agent directory as a fallback when the active agent's own skills directory couldn't be resolved at all. Now it always scans every known agent directory (in addition to any explicitly resolved/passed one), so skills rendered for a non-active agent aren't left behind after extension remove. Kept registered_skills as a flat, deduped list rather than switching to a per-agent dict the existing SKILL.md metadata.source ownership check already makes a full-scan-based cleanup safe, and a schema change would be a much larger, riskier diff. Added test_remove_cleans_up_skills_for_non_active_agent to cover this.

On the first point (making plain integration install immediately register extension commands/skills for the newly-installed agent): I implemented this, but it directly contradicts the existing, explicitly-tested design from #2886 see test_install_defers_extension_commands_until_use and test_install_skills_mode_secondary_agent_defers_extension_artifacts in tests/integrations/test_integration_subcommand.py, whose docstrings say "Maintainer-requested behavior for #2886: extension command back-fill is limited to integration use/switch/upgrade. Plain install only adds the integration..."

Since that looks like a deliberate, previously-approved decision, I didn't want to silently reverse it based on an automated review comment. @mnriem should install now also immediately back-fill extension artifacts for a skills-mode secondary agent (superseding that part of #2886), or should the existing deferred-registration behavior stay as-is and #2948 be considered fully addressed by use/switch/upgrade/extension add (all already fixed in this PR)? Happy to implement whichever you prefer.

pytest tests/ -k extension: 620/620 pass. git diff --check: clean.

@mnriem

mnriem commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

@mnriem Pushed another commit addressing Copilot's second point (skill cleanup across agents):

  • Orphaned skills on remove: _unregister_extension_skills() previously only scanned every known agent directory as a fallback when the active agent's own skills directory couldn't be resolved at all. Now it always scans every known agent directory (in addition to any explicitly resolved/passed one), so skills rendered for a non-active agent aren't left behind after extension remove. Kept registered_skills as a flat, deduped list rather than switching to a per-agent dict the existing SKILL.md metadata.source ownership check already makes a full-scan-based cleanup safe, and a schema change would be a much larger, riskier diff. Added test_remove_cleans_up_skills_for_non_active_agent to cover this.

On the first point (making plain integration install immediately register extension commands/skills for the newly-installed agent): I implemented this, but it directly contradicts the existing, explicitly-tested design from #2886 see test_install_defers_extension_commands_until_use and test_install_skills_mode_secondary_agent_defers_extension_artifacts in tests/integrations/test_integration_subcommand.py, whose docstrings say "Maintainer-requested behavior for #2886: extension command back-fill is limited to integration use/switch/upgrade. Plain install only adds the integration..."

The existing behavior should be retained

Since that looks like a deliberate, previously-approved decision, I didn't want to silently reverse it based on an automated review comment. @mnriem should install now also immediately back-fill extension artifacts for a skills-mode secondary agent (superseding that part of #2886), or should the existing deferred-registration behavior stay as-is and #2948 be considered fully addressed by use/switch/upgrade/extension add (all already fixed in this PR)? Happy to implement whichever you prefer.

pytest tests/ -k extension: 620/620 pass. git diff --check: clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/specify_cli/integrations/_helpers.py:410

  • Plain integration install still never invokes this helper (the docstring immediately above says install deliberately has no extension side effects, and the call sites are only use/switch/upgrade). Therefore the issue's reproduction—add the extension, then install non-active Copilot with --skills --force—still leaves Copilot without the extension skill. Wire the install path into this agent-aware registration, or otherwise reconcile the stated install acceptance criterion.
    Extension *skill* rendering is scoped to ``agent_key`` itself (using
    per-agent settings in ``.specify/integration.json`` when available, with
    a fallback to the legacy global init-options for the active agent), so a
    skills-mode agent registered while it is not the active agent (e.g.
    Copilot ``--skills`` registered while non-active) still receives skill
    files here instead of only command files (#2948).

src/specify_cli/extensions/init.py:1574

  • This second pass adds skills but does not prevent the preceding register_commands_for_all_agents() pass from writing command files for a non-active skills-mode agent. That registrar only recognizes skills mode from the active init-options agent (agents.py:1008-1010), so an installed Copilot with .github/agents present receives both .agent.md and SKILL.md, contrary to the acceptance criterion. The new test masks this by calling install_from_directory(..., register_commands=False); the normal extension-add path needs to filter per-agent command registration and assert the .agent.md file is absent.
        # Auto-register extension commands as agent skills for every
        # skills-mode agent detected (active agent plus any other
        # installed integrations in skills mode), not just the active
        # agent (#2948).
        registered_skills = self._register_extension_skills_for_installed_agents(
            manifest, dest_dir, link_outputs=link_commands
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment on lines +1368 to +1374
# Additionally always scan every other known agent skills
# directory: skills may have been rendered for more than one
# agent (#2948), so limiting cleanup to only the directory above
# would orphan skills registered for a non-active agent.
# Directories already handled above are silently skipped since
# their matching skill subdirectories no longer exist.
if True:
@mnriem

mnriem commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Address Copilot feedback and fix test & lint errors. Note that cleanup should never touch any files it does not own. So if a specific integration is uninstalled it should only cleanup those files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extension commands aren't rendered as skills for non-active skills-mode agents on add/install/upgrade

4 participants